/*******************************************************************************
 * Copyright (c) 2011 Prometheus Consulting
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/

package nz.co.senanque.vaadinsupport.application;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.annotation.PreDestroy;

import nz.co.senanque.localemanagement.LocaleAwareRuntimeException;
import nz.co.senanque.vaadinsupport.ButtonPainter;
import nz.co.senanque.vaadinsupport.ButtonProperty;
import nz.co.senanque.vaadinsupport.CommandExt;
import nz.co.senanque.vaadinsupport.FieldFactory;
import nz.co.senanque.vaadinsupport.Hints;
import nz.co.senanque.vaadinsupport.LabelProperty;
import nz.co.senanque.vaadinsupport.MaduraForm;
import nz.co.senanque.vaadinsupport.MaduraPropertyWrapper;
import nz.co.senanque.vaadinsupport.MenuItemPainter;
import nz.co.senanque.vaadinsupport.permissionmanager.PermissionManager;
import nz.co.senanque.validationengine.FieldMetadata;
import nz.co.senanque.validationengine.ObjectMetadata;
import nz.co.senanque.validationengine.ProxyField;
import nz.co.senanque.validationengine.ValidationEngine;
import nz.co.senanque.validationengine.ValidationObject;
import nz.co.senanque.validationengine.ValidationSession;
import nz.co.senanque.validationengine.choicelists.ChoiceBase;

import org.apache.commons.beanutils.PropertyUtilsBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.MessageSourceAccessor;

import com.vaadin.data.Item;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.terminal.UserError;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.AbstractField;
import com.vaadin.ui.AbstractSelect;
import com.vaadin.ui.Button;
import com.vaadin.ui.Field;
import com.vaadin.ui.Label;
import com.vaadin.ui.MenuBar.Command;
import com.vaadin.ui.MenuBar.MenuItem;

/**
 * 
 * All of the Madura session management is done here. You register fields (or whole forms) with the manager.
 * Then you bind specific instances of the object graph to these fields.
 * There is one of these per Madura Session
 * 
 * @author Roger Parkinson
 * @version $Revision:$
 */
public class MaduraSessionManager implements Serializable, MessageSourceAware, InitializingBean
{
	private static final long serialVersionUID = 1415805577188960471L;
	private static Logger logger = LoggerFactory.getLogger(MaduraSessionManager.class);
	private List<AbstractField> m_fields = new ArrayList<AbstractField>();
	private List<Label> m_labels = new ArrayList<Label>();
    @Autowired(required=false) private ValidationEngine m_validationEngine;
    @Autowired(required=false) private ValidationSession m_validationSession;
    @Autowired private FieldFactory m_formFieldFactory;
    @Autowired PermissionManager m_permissionManager;
    @Autowired private Hints m_hints;
	private MessageSourceAccessor m_messageSourceAccessor;
    
    private class MenuItemWrapper extends AbstractField {
    	
    	private static final long serialVersionUID = 418011465566857902L;
		private final MenuItemPainter m_menuItemPainter;

		public Class<?> getType() {
			return null;
		}
		
		private MenuItemWrapper(MenuItem menuItem, MenuItemPainter menuItemPainter)
		{
			m_menuItemPainter = menuItemPainter;
			setData(menuItem);
		}

		public MenuItemPainter getMenuItemPainter() {
			return m_menuItemPainter;
		}
    }

	private void registerWidget(AbstractField field) {
		m_fields.add(field);
	}
	private void registerWidget(MenuItem field, MenuItemPainter bp) {
		for (AbstractField foundField: m_fields)
		{
			if (foundField instanceof MenuItemWrapper)
			{
				MenuItemWrapper menuItemWrapper = (MenuItemWrapper)foundField;
				if (menuItemWrapper.getMenuItemPainter() == bp)
				{
					// we found this item already in the list so reuse it
					// rather than create a new entry. This saves us having to deregister and
					// recreate whenever the menu reconfigures.
					menuItemWrapper.setData(field);
					return;
				}
			}
		}
		m_fields.add(new MenuItemWrapper(field, bp));
	}
	public void register(Label field) {
		m_labels.add(field);
	}
	public void deregister(AbstractComponent field) {
		m_fields.remove(field);
		m_labels.remove(field);
	}

	/**
	 * Check all the other fields to ensure they are still valid
	 * this includes any buttons that were registered because they
	 * get disabled if there are errors on the relevant form or
	 * if the requiredness is incomplete.
	 * 
	 * @param field
	 */
	public void updateOtherFields(AbstractField field) {
		PermissionManager permissionmanager = getPermissionManager();
		for (Label fieldx: m_labels)
		{
		    com.vaadin.data.Property p = fieldx.getPropertyDataSource();
			if (p != null && p instanceof LabelProperty)
			{
				fieldx.requestRepaint();
			}
		}
		for (AbstractField fieldx: m_fields)
		{
			if (fieldx.equals(field)) continue;
			if (fieldx instanceof Button)
			{
			    com.vaadin.data.Property p = fieldx.getPropertyDataSource();
				if (p != null && p instanceof ButtonProperty)
				{
					((ButtonProperty)p).getPainter().paint((Button)fieldx);
					fieldx.requestRepaint();
				}
				continue;
			}
			if (fieldx instanceof MenuItemWrapper)
			{
				MenuItemPainter menuItemPainter = ((MenuItemWrapper)fieldx).getMenuItemPainter();
			    MenuItem menuItem = (MenuItem)fieldx.getData();
				if (menuItemPainter != null)
				{
					menuItemPainter.paint(menuItem);
					fieldx.requestRepaint();
				}
				continue;
			}
			MaduraPropertyWrapper property = (MaduraPropertyWrapper)fieldx.getPropertyDataSource();
			if (property != null)
			{
				if (logger.isDebugEnabled())
				{
					logger.debug("evaluating field: {}",property.getName());
					if (fieldx.isEnabled()!=property.isEnabled())
					{
						logger.debug("Enabled: {} {}",fieldx.isEnabled(),property.isEnabled());
					}
					if (fieldx.isReadOnly()!=property.isReadOnly())
					{
						logger.debug("ReadOnly: {} {}",fieldx.isReadOnly(),property.isReadOnly());
					}
					if (fieldx.isRequired()!=property.isRequired())
					{
						logger.debug("Required: {} {}",fieldx.isRequired(),property.isRequired());
					}
					if (fieldx.isVisible()!=property.isVisible())
					{
						logger.debug("Visible: {} {}",fieldx.isVisible(),property.isVisible());
					}
				}
				fieldx.setEnabled(property.isEnabled());
				fieldx.setReadOnly(property.isReadOnly());
				fieldx.setRequired(property.isRequired());
				fieldx.setVisible(property.isVisible());
				// Permissions trump rules
		        if (!permissionmanager.hasPermission(property.getReadPermission()))
		        {
		            fieldx.setVisible(false);
		        }
		        if (!permissionmanager.hasPermission(property.getWritePermission()))
		        {
		            fieldx.setEnabled(false);
		        }
				if (fieldx instanceof AbstractSelect)
				{
					AbstractSelect select = (AbstractSelect)fieldx;
					List<ChoiceBase> availableList = new ArrayList<ChoiceBase>();
					for (ChoiceBase v:property.getAvailableValues())
					{
						availableList.add(v);
					}
	                logger.debug("{} availableList {}",property.getName(), availableList);
					Collection<?> itemIds = select.getItemIds();
					List<Object> killList = new ArrayList<Object>();
					for (Object itemId:itemIds)
					{
						if (availableList.contains(itemId)) continue;
						killList.add(itemId);
					}
					for (Object kill: killList)
					{
						select.removeItem(kill);
					}
					for (ChoiceBase cb: availableList)
					{
						select.addItem(cb);
					}
					logger.debug("Select {} value \"{}\", updated to {}",new Object[]{property.getName(), select.getValue(), select.getItemIds()});
				}
			}
			fieldx.requestRepaint();
		}
	}

	public Hints getHints() {
		return m_hints;
	}

	public void setHints(Hints hints) {
		m_hints = hints;
	}
/////////////////////////////////////////////////
    public List<MaduraPropertyWrapper> getFieldList(ValidationObject validationObject)
    {
        List<MaduraPropertyWrapper> ret = new ArrayList<MaduraPropertyWrapper>();
        PropertyDescriptor[] propertyDescriptors = new PropertyUtilsBean()
                .getPropertyDescriptors(validationObject);
        // ObjectMetadata objectMetadata = validationObject.getMetadata();
        for (PropertyDescriptor propertyDescriptor : propertyDescriptors)
        {
            if (!isUsefulField(propertyDescriptor))
            {
                continue;
            }
            String property = propertyDescriptor.getName();
            Method getter = propertyDescriptor.getReadMethod();
            Method setter = propertyDescriptor.getWriteMethod();
            FieldMetadata fieldMetadata = validationObject.getMetadata()
                    .getFieldMetadata(property);
            ret.add(new MaduraPropertyWrapper(fieldMetadata,
                    validationObject, setter, getter, getHints()));
        }
        return ret;
    }
    public List<String> getFieldList(ValidationObject validationObject, Item dataSource)
    {
        List<String> ret = new ArrayList<String>();
        PropertyDescriptor[] propertyDescriptors = new PropertyUtilsBean().getPropertyDescriptors(validationObject);
        ObjectMetadata objectMetadata = validationObject.getMetadata();
        for (PropertyDescriptor propertyDescriptor: propertyDescriptors)
        {
            if (!isUsefulField(propertyDescriptor))
            {
                continue;
            }
            String property = propertyDescriptor.getName();
            Method getter = propertyDescriptor.getReadMethod();
            Method setter = propertyDescriptor.getWriteMethod();
            FieldMetadata fieldMetadata;
            try
            {
                fieldMetadata = objectMetadata.getFieldMetadata(property);
                dataSource.removeItemProperty(property);
                dataSource.addItemProperty(property, new MaduraPropertyWrapper(fieldMetadata,validationObject,setter, getter, getHints()));
            }
            catch (NullPointerException e)
            {
                logger.warn("property {} not bound to Madura Objects",property);
            }
            ret.add(property);
        }
        return ret;
    }
    public boolean isUsefulField(PropertyDescriptor propertyDescriptor)
    {
        String property = propertyDescriptor.getName();
        if (property.equals("validationSession"))
        {
            return false;
        }
        if (property.equals("class"))
        {
            return false;
        }
        if (property.equals("id"))
        {
            return false;
        }
        if (property.equals("parentid"))
        {
            return false;
        }
        if (property.equals("metadata"))
        {
            return false;
        }
        if (property.equals("version"))
        {
            return false;
        }
        Class<?> clazz = propertyDescriptor.getPropertyType();
        if (List.class.isAssignableFrom(clazz))
        {
            return false;
        }
        return true;
    }  
    public MaduraPropertyWrapper getMaduraPropertyWrapper(FieldMetadata fieldMetadata)
    {
    	ProxyField pf = fieldMetadata.getValidationSession().getProxyField(fieldMetadata);
    	return getMaduraPropertyWrapper((ValidationObject)pf.getProxyObject().getObject(),pf.getFieldName());
    }
    public MaduraPropertyWrapper getMaduraPropertyWrapper(ValidationObject validationObject,
            String propertyName)
    {
        Hints hints = getHints();
        PropertyDescriptor[] propertyDescriptors = new PropertyUtilsBean()
                .getPropertyDescriptors(validationObject);
        ObjectMetadata objectMetadata = validationObject.getMetadata();
        for (PropertyDescriptor propertyDescriptor : propertyDescriptors)
        {
            String property = propertyDescriptor.getName();
            if (!property.equals(propertyName))
            {
                continue;
            }
            Method getter = propertyDescriptor.getReadMethod();
            Method setter = propertyDescriptor.getWriteMethod();
            FieldMetadata fieldMetadata;
            try
            {
                fieldMetadata = objectMetadata.getFieldMetadata(property);
                return new MaduraPropertyWrapper(fieldMetadata, validationObject, setter,
                        getter, hints);
            }
            catch (NullPointerException e)
            {
                return new MaduraPropertyWrapper(propertyName, validationObject, setter,
                        getter, hints);
            }
        }
        return null;
    }

    public void bind(final MaduraForm form, final AbstractField field,
            ValidationObject validationObject, String propertyName)
    {
        MaduraPropertyWrapper property = getMaduraPropertyWrapper(validationObject, propertyName);
        if (property == null)
        {
            throw new LocaleAwareRuntimeException("property.not.found",
                    new Object[]
                    { validationObject.getClass().getName(), propertyName },m_messageSourceAccessor);
        }
        bind(form, field, property);
    }

    public void bind(final MaduraForm form, final AbstractField field,
            MaduraPropertyWrapper property)
    {
        field.setPropertyDataSource(property);
        Hints hints = getHints();

        hints.setCommonProperties(field, property);
        setPermissions(property, field);
        registerWidget(field);
        field.addListener(new MaduraPropertyWrapper.ValueChangeListener()
        {

			private static final long serialVersionUID = -3295559168401789196L;

			public void valueChange(ValueChangeEvent event)
            {
                com.vaadin.data.Property p = field.getPropertyDataSource();
                if (p instanceof MaduraPropertyWrapper)
                {
                    MaduraPropertyWrapper property = (MaduraPropertyWrapper) p;
                    if (property.getErrorText() != null)
                    {
                        field.setComponentError(new UserError(property
                                .getErrorText()));
                    }
                    else
                    {
                        field.setComponentError(null);
                    }
                }
                List<String> errors = new ArrayList<String>();
                if (form != null)
                {
                    for (Object propertyId : form.getItemPropertyIds())
                    {
                        Field f = form.getField(propertyId);
                        if (f instanceof AbstractField)
                        {
                            AbstractField fieldy = (AbstractField) f;
                            if (fieldy.getComponentError() != null)
                            {
                                errors.add(fieldy.getComponentError()
                                        .toString());
                            }
                        }
                    }
                    form.setErrors(errors);
                }
                updateOtherFields(field);
            }
        });
    }
    public void register(final AbstractField field)
    {
//        register(field,null);
//    }
//    public void register(final AbstractField field, final MaduraForm form)
//    {
        if (field instanceof Button)
        {
            throw new RuntimeException("Attempted to register a button without a Button Painter");
        }
        registerWidget(field);
        field.addListener(new MaduraPropertyWrapper.ValueChangeListener()
        {

            private static final long serialVersionUID = 5542293169155226281L;

			public void valueChange(ValueChangeEvent event)
            {
                com.vaadin.data.Property p = field.getPropertyDataSource();
                if (p instanceof MaduraPropertyWrapper)
                {
                    MaduraPropertyWrapper property = (MaduraPropertyWrapper) p;
                    if (property.getErrorText() != null)
                    {
                        field.setComponentError(new UserError(property
                                .getErrorText()));
                    }
                    else
                    {
                        field.setComponentError(null);
                    }
                }
//                List<String> errors = new ArrayList<String>();
//                if (form != null)
//                {
//                    for (Object propertyId : form.getItemPropertyIds())
//                    {
//                        Field f = form.getField(propertyId);
//                        if (f instanceof AbstractField)
//                        {
//                            AbstractField fieldy = (AbstractField) f;
//                            if (fieldy.getComponentError() != null)
//                            {
//                                errors.add(fieldy.getComponentError()
//                                        .toString());
//                            }
//                        }
//                    }
//                    form.setErrors(errors);
//                }
                updateOtherFields(field);
            }
        });
    }

    public void register(final Button field, ButtonPainter painter)
    {
        MaduraPropertyWrapper property = painter.getProperty();        
        ButtonProperty bp = new ButtonProperty(painter,field.getCaption(),m_messageSourceAccessor);
        field.setPropertyDataSource(bp);
        if (property != null)
        {
            Hints hints = getHints();
            hints.setCommonProperties(field, property);
            setPermissions(property, field);
        }
        registerWidget(field);
    }
    
    public void register(final MenuItem field, MenuItemPainter painter)
    {
        MaduraPropertyWrapper property = painter.getProperty();
        if (property != null)
        {
            Hints hints = getHints();
            hints.setCommonProperties(field, property);
            setPermissions(property, field);
        }
        registerWidget(field,painter);
    }
    
    public void register(final MenuItem menuItem)
    {
    	Command command = menuItem.getCommand();
    	if (command instanceof CommandExt)
    	{
    		CommandExt commandExt = (CommandExt)command;
    		MenuItemPainter painter = commandExt.getPainter();
            MaduraPropertyWrapper property = painter.getProperty();
            if (property != null)
            {
                Hints hints = getHints();
                hints.setCommonProperties(menuItem, property);
                setPermissions(property, menuItem);
            }
            registerWidget(menuItem,painter);
    	}
    }
    
    public void bind (final Button button, List<MaduraPropertyWrapper> properties)
    {
        ButtonProperty buttonProperty = (ButtonProperty)button.getPropertyDataSource();
        buttonProperty.getPainter().setProperties(properties);
        button.setCaption(buttonProperty.getCaption());
        buttonProperty.getPainter().paint(button);  
    }

    public void bind (final MenuItem menuItem, List<MaduraPropertyWrapper> properties)
    {
    	Command command = menuItem.getCommand();
    	if (command instanceof CommandExt)
    	{
    		CommandExt commandExt = (CommandExt)command;
    		MenuItemPainter painter = commandExt.getPainter();
    		painter.setProperties(properties);
    		painter.paint(menuItem);
    	}
    }

    private void setPermissions(MaduraPropertyWrapper property, AbstractComponent field)
    {
		PermissionManager permissionmanager = getPermissionManager();
        if (!permissionmanager.hasPermission(
                property.getWritePermission()))
        {
            field.setEnabled(false);
        }
        if (!permissionmanager.hasPermission(
                property.getReadPermission()))
        {
            field.setVisible(false);
        }
    }

    private void setPermissions(MaduraPropertyWrapper property, MenuItem field)
    {
		PermissionManager permissionmanager = getPermissionManager();
        if (!permissionmanager.hasPermission(
                property.getWritePermission()))
        {
            field.setEnabled(false);
        }
        if (!permissionmanager.hasPermission(
                property.getReadPermission()))
        {
            field.setVisible(false);
        }
    }

    public void bind(AbstractField field, String propertyName,
            List<MaduraPropertyWrapper> properties) {
        MaduraPropertyWrapper property = findProperty(propertyName,properties);
        bind(field,property);
    }
    public void bind(final AbstractField field,
            MaduraPropertyWrapper property)
    {
        field.setPropertyDataSource(property);
        Hints hints = getHints();

        hints.setCommonProperties(field, property);
        setPermissions(property, field);
    }
    
    public MaduraPropertyWrapper findProperty(String propertyName, List<MaduraPropertyWrapper> properties)
    {
        for (MaduraPropertyWrapper property: properties)
        {
            if (property.getName().equals(propertyName))
            {
                return property;
            }
        }
        throw new LocaleAwareRuntimeException("Property named {0} not found in list", new Object[]{propertyName},m_messageSourceAccessor);
    }
    
    public void bind(Label field, LabelProperty property) {
        field.setPropertyDataSource(property);
        setPermissions(property.getProperty(), field);
    }
    @PreDestroy
    public void close()
    {
        if (m_validationSession == null)
        {
        	return;
        }
        m_validationSession.close();
        m_validationSession = null;
    }
    public ValidationEngine getValidationEngine()
    {
        return m_validationEngine;
    }
    public void setValidationEngine(ValidationEngine validationEngine)
    {
        m_validationEngine = validationEngine;
    }
    public ValidationSession getValidationSession()
    {
        if (m_validationSession == null)
        {
            m_validationSession = getValidationEngine().createSession(LocaleContextHolder.getLocale());
        }
        return m_validationSession;
    }
    /* (non-Javadoc)
     * @see nz.co.senanque.vaadinsupport.viewmanager.ViewManager#getFieldFactory()
     */
    public FieldFactory getFieldFactory() {
        return m_formFieldFactory;
    }
    /* (non-Javadoc)
     * @see nz.co.senanque.vaadinsupport.viewmanager.ViewManager#getFormFieldFactory()
     */
    public FieldFactory getFormFieldFactory() {
        return m_formFieldFactory;
    }
    public void setFormFieldFactory(FieldFactory formFieldFactory) {
        m_formFieldFactory = formFieldFactory;
    }
	public void setMessageSource(MessageSource messageSource) {
        m_messageSourceAccessor = new MessageSourceAccessor(messageSource);
	}
	public PermissionManager getPermissionManager() {
		return m_permissionManager;
	}
	public void setPermissionManager(PermissionManager permissionManager) {
		m_permissionManager = permissionManager;
	}
	public MessageSourceAccessor getMessageSourceAccessor() {
		return m_messageSourceAccessor;
	}
	public void afterPropertiesSet() throws Exception {
		m_formFieldFactory.setMaduraSessionManager(this);
	}
}
